/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package xenon3d;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Rectangle;
import javax.media.opengl.awt.GLCanvas;
import javax.media.opengl.GLCapabilities;
import xenon3d.scene.RestrictedAccessException;

/**
 * The Canvas3D class provides a drawing canvas for 3D rendering. It is used
 * either for on-screen rendering or off-screen rendering.<p>
 * NOTE: the rendering process automatically starts as soon as a the Canvas3D
 * is attached to a Container. By default, the rendering is only done whenever a
 * paint event for the Canvas3D occours. For most 3D applications, it will be
 * more appropriate for the rendering to occour as fast as possible or at fixed
 * time intervalls. Use the start() and stop() methods to turn fast rendering on
 * or off.
 * @author Volker Everts
 * @version 0.1 - 13.08.2011: Created
 */
public class Canvas3D implements RenderListener {

    // <editor-fold defaultstate="collapsed" desc=" Private Fields ">
    
    /** The internal GraphicsConfiguration object. */
    private GraphicsConfiguration gc;
    
    /** The offScreen flag. */
    private boolean offScreen;
    
    /** The internal GLCapabilities object. */
    private GLCapabilities caps;

    /** The internal GLCanvas object. */
    private GLCanvas canvas;

    /** The internal View3D object. */
    private View3D view;

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Initialization ">

    /**
     * Creates a new Canvas3D suitable for the current display settings. The
     * following Canvas3D attributes are initialized to default values as shown:
     * <ul>
     * <li>Off-Screen: No (not supported)</li>
     * <li>Stereo: No (not supported)</li>
     * <li>Double Buffer: Yes (if available)</li>
     * <li>Hardware Acceleration: Yes (if available)</li>
     * <li>Scene Antialiasing: No</li>
     * <li>Depth Buffering: Yes</li>
     * </ul>
     */
    public Canvas3D() {
        this(null, false);
    }
    
    /**
     * Creates a new Canvas3D that is optimized for the specified graphics
     * configuration.<p>
     * NOTE: the GraphicsConfiguration object should be created using a
     * GraphicsConfigTemplate3D
     * @param gc a valid GraphicsConfiguration object that will be used to
     * create the canvas
     */
    public Canvas3D(GraphicsConfiguration gc) {
        this(gc, false);
    }
    
    /**
     * Creates a new Canvas3D that is optimized for the specified graphics
     * configuration. The GraphicsConfiguration object should be created using a
     * GraphicsConfigTemplate3D.<p>
     * Note that if offScreen is set to true, this Canvas3D object cannot be
     * used for normal rendering; it should not be aattached to any Container
     * object.
     * @param gc a valid GraphicsConfiguration object that will be used to
     * create the canvas
     * @param offScreen a flag that indicates whether this canvas is an
     * off-screen 3D rendering canvas
     */
    public Canvas3D(GraphicsConfiguration gc, boolean offScreen) {
        // TODO: Off-screen rendering
        if (offScreen) throw new UnsupportedOperationException(Xenon3D.ERR_OFFSCREEN_CANVAS);
        if (gc == GraphicsConfigTemplate3D.bestGC) caps = GraphicsConfigTemplate3D.bestCaps;
        else {
            if (gc == null) {
                GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
                GraphicsDevice gd = ge.getDefaultScreenDevice();
                gc = gd.getDefaultConfiguration();
            }
            caps = new GLCapabilities(Xenon3D.getProfile());
            caps.setDoubleBuffered(isDoubleBufferAvailable());
            caps.setHardwareAccelerated(isHardwareAccelerationAvailable());
        } 
        this.gc = gc;
        this.offScreen = offScreen;
    }
    
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (General) ">
    
    /**
     * Returns the attached View3D object, or null, if no view is attached.
     * @return the attached View3D
     */
    public View3D getView() {
        return view;
    }
    
    /**
     * Returns the 2D graphics object associated with this Canvas3D.
     * @return a Graphics2D object that can be used for Java 2D rendering into
     * this Canvas3D
     */
    public Graphics2D getGraphics2D() {
        if (canvas == null) return null;
        return (Graphics2D) canvas.getGraphics();
    }
    
    /**
     * Returns the parent container of this Canvas3D.
     * @return the parent container, or null, if this Canvas3D is not attached
     * to a container
     */
    public final Container getParent() {
        if (canvas == null) return null;
        return canvas.getParent();
    }

    /**
     * Gets the bounds rectangel of thic Canvas3D.
     * @return the bounds as a Rectangle object, or null, if this Canvas3D is
     * not attached to a container
     */
    public final Rectangle getBounds() {
        if (canvas == null) return null;
        return canvas.getBounds();
    }

    /**
     * Gets the size of this Canvas3D.
     * @return the canvas size, or null, if this Canvas3D is not attached to
     * a container
     */
    public final Dimension getSize() {
        if (canvas == null) return null;
        return canvas.getSize();
    }

    /**
     * Gets the location of this Canvas3D within its Container.
     * @return the location, or null, if this Canvas3D is not attached to
     * a container
     */
    public final Point getLocation() {
        if (canvas == null) return null;
        return canvas.getLocation();
    }

    /**
     * Gets the width of this Canvas3D.
     * @return the width, or -1 if this Canvas3D is not attached to a container
     */
    public final int getWidth() {
        if (canvas == null) return -1;
        return canvas.getWidth();
    }

    /**
     * Gets the height of this Canvas3D.
     * @return the height, or -1, if this Canvas3D is not attached to a container
     */
    public final int getHeight() {
        if (canvas == null) return -1;
        return canvas.getHeight();
    }

    /**
     * Gets the x coordinate of this Canvas3D.
     * @return the x coordinate, or -1, if this Canvas3D is not attached to a
     * container
     */
    public final int getX() {
        if (canvas == null) return -1;
        return canvas.getX();
    }

    /**
     * Gets the y coordinate of this Canvas3D.
     * @return the y coordinate, or -1, if this Canvas3D is not attached to a
     * container
     */
    public final int getY() {
        if (canvas == null) return -1;
        return canvas.getY();
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Properties (3D) ">
    
    /**
     * Returns a status flag indicating whether or not double buffering is
     * available.
     * @return true, if double buffering is available
     */
    public final boolean isDoubleBufferAvailable() {
        return gc.getBufferCapabilities().isPageFlipping();
    }
    
    /**
     * Returns a status flag indicating whether or not double buffering is
     * enabled. Default: true.
     * @return true, if double buffering is enabled
     */
    public final boolean isDoubleBufferEnabled() {
        return caps.getDoubleBuffered();
    }

    /**
     * Sets whether or not double buffering is enabled. Default: true. If double
     * buffering is off, all drawing is to the front buffer and no buffer swap
     * is done between frames. Running Xenon3D with double buffering disabled is
     * not recommended. Enabling double buffering on a Canvas3D that does not
     * support double buffering has no effect. 
     * @param enable if true, double buffering will be enabled
     * @throws IllegalStateException if the Canvas3D is already attached to a Container
     */
    public final void setDoubleBufferEnabled(boolean enable) {
        if (canvas != null) throw new RestrictedAccessException();
        if (isDoubleBufferAvailable() || !enable) caps.setDoubleBuffered(enable);
    }
    
    /**
     * Returns a status flag indicating whether or not scene antialiasing is
     * enabled. Default: false.
     * @return true, if scene antialiasing is enabled
     */
    public final boolean isSceneAntialiasingEnabled() {
        return caps.getSampleBuffers();
    }

    /**
     * Sets whether or not scene antialiasing is enabled. Default: false.
     * @param enable if true, scene antialiasing will be enabled
     * @throws IllegalStateException if the Canvas3D is already attached to a Container
     */
    public final void setSceneAntialiasingEnabled(boolean enable) {
        if (canvas != null) throw new IllegalStateException(Xenon3D.ERR_CANVAS_ALREADY_ATTACHED);
        caps.setSampleBuffers(enable);
    }
    
    /**
     * Returns a status flag indicating whether or not stereo is available.
     * @return if true, stereo rendering is available
     */
    public final boolean isStereoAvailable() {
        return gc.getBufferCapabilities().isMultiBufferAvailable();
    }
    
    /**
     * Returns a status flag indicating whether or not stereo is enabled.
     * Default: false.
     * @return true, if stereo is enabled
     */
    public final boolean isStereoEnabled() {
        return caps.getStereo();
    }

    /**
     * Sets whether or not stereo is enabled. Default: false. Note that this
     * attribute is used only when stereo is available. Enabling stereo on a
     * Canvas3D that does not support stereo has no effect. 
     * @param enable if true, stereo will be enabled
     * @throws IllegalStateException if the Canvas3D is already attached to a Container
     */
    public final void setStereoEnabled(boolean enable) {
        if (canvas != null) throw new IllegalStateException(Xenon3D.ERR_CANVAS_ALREADY_ATTACHED);
        if (isStereoAvailable() || !enable) caps.setStereo(enable);
    }
    
    /**
     * Returns a status flag indicating whether or not hardware acceleration is
     * available.
     * @return if true, hardware acceleration is available
     */
    public final boolean isHardwareAccelerationAvailable() {
        return gc.getImageCapabilities().isAccelerated();
    }
    
    /**
     * Returns a status flag indicating whether or not hardware acceleration is
     * enabled. Default: true.
     * @return true, if hardware acceleration is enabled
     */
    public final boolean isHardwareAccelerationEnabled() {
        return caps.getHardwareAccelerated();
    }

    /**
     * Sets whether or not hardware acceleration is enabled. Default: true.
     * Note that this attribute is used only when hardware acceleration is
     * available. Enabling hardware acceleration on a Canvas3D that does not
     * support it has no effect. 
     * @param enable if true, hardware acceleration will be enabled
     * @throws IllegalStateException if the Canvas3D is already attached to a Container
     */
    public final void setHardwareAccelerationEnabled(boolean enable) {
        if (canvas != null) throw new IllegalStateException(Xenon3D.ERR_CANVAS_ALREADY_ATTACHED);
        if (isHardwareAccelerationAvailable() || !enable) caps.setHardwareAccelerated(enable);
    }

    /**
     * Returns a status flag indicating whether or not the depth buffer is
     * enabled. Default: true.
     * @return true, if the depth buffer is initially enabled
     */
    public final boolean isDepthBufferEnabled() {
        return caps.getDepthBits() > 0;
    }

    /**
     * Sets whether or not the depth buffer is enabled. Default: true.<p>
     * NOTE: the actual number of depth buffer bits that is used depends on
     * the OpenGL implementation.
     * @param enable if true, the depth buffer will be enabled
     * @throws IllegalStateException if the Canvas3D is already attached to a Container
     */
    public final void setDepthBufferEnabled(boolean enable) {
        if (canvas != null) throw new IllegalStateException(Xenon3D.ERR_CANVAS_ALREADY_ATTACHED);
        caps.setDepthBits(16);
    }

    /**
     * Returns a status flag indicating whether or not the stencil buffer is
     * enabled for this Canvas3D. Default: false.
     * @return true, if the stencil buffer is enabled
     */
    public final boolean isStencilBufferEnabled() {
        return caps.getStencilBits() > 0;
    }

    /**
     * Sets whether or not the stencil buffer will be enabled. Default: false.<p>
     * NOTE: the actual number of stencil buffer bits that is used depends on
     * the OpenGL implementation.
     * @param enable if true, the stencil buffer will be enabled
     * @throws IllegalStateException if the Canvas3D is already attached to a Container
     */
    public final void setStencilBufferEnabled(boolean enable) {
        if (canvas != null) throw new IllegalStateException(Xenon3D.ERR_CANVAS_ALREADY_ATTACHED);
        int bits = enable ? 8 : 0;
        caps.setStencilBits(bits);
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Public Methods (General) ">

    /**
     * Attaches the specified View3D object to this canvas.
     * @param view the View3D to attach
     */
    public void attachView(View3D view) {
        if (view == null) throw new NullPointerException();
        if (this.view != null) removeView();
        view.addCanvas(this);
        if (canvas != null) canvas.addGLEventListener(view.getInternalListener());
        this.view = view;
    }

    /**
     * Removes any attached View3D object from this Canvas3D.
     */
    public void removeView() {
        if (view == null) return;
        if (canvas != null) canvas.removeGLEventListener(view.getInternalListener());
        view.removeCanvas();
        view = null;
    }

    // </editor-fold>
    
    // <editor-fold defaultstate="collapsed" desc=" Public Methods (Render Listener) ">
    
    /**
     * Called immediately after the OpenGL context is initialized. Applications
     * that wish to perform operations before the start of the rendering loop,
     * may override this method.<p>
     * NOTE: Applications should not call this method directly. It is called by
     * the Xenon3D rendering system.
     */
    public void init() {}
    
    /**
     * Called at the begin of the rendering loop, after clearing the canvas and
     * before any rendering has been done for this frame. Applications that wish
     * to perform operations in the rendering loop, prior to any actual
     * rendering, may override this method.<p>
     * NOTE: Applications should not call this method directly. It is called by
     * the Xenon3D rendering system.
     */
    public void preRender() {}
    
    /**
     * Called during the execution of the rendering loop, right after the solid
     * rendering pass and before the transparent rendering pass. Furthermore, it
     * is called once for each field (i.e., once per frame on a mono system or
     * once each for the right eye and left eye on a two-pass stereo system.
     * Applications that wish to perform operations during the rendering loop,
     * may override this method.<p>
     * NOTE: Applications should not call this method directly. It is called by
     * the Xenon3D rendering system.
     */
    public void render() {}

    /**
     * Called at the end of each rendering loop after completing all rendering
     * to the canvas for this frame and before the buffer swap. Applications
     * that wish to perform operations in the rendering loop, following any
     * actual rendering may override this method.<p>
     * NOTE: Applications should not call this method directly. It is called by
     * the Xenon3D rendering system.
     */
    public void postRender() {}

    /**
     * Called during the first repaint after the Canvas3D has been resized. The
     * client can update certain settings appropriately. Note that for
     * convenience the component has already called glViewport(x, y, width,
     * height) when this method is called, so the client may not have to do
     * anything in this method. Applications that wish to perform operations in
     * case of the resizing of the Canvas3D may override this method.<p>
     * NOTE: Applications should not call this method directly. It is called by
     * the Xenon3D rendering system.
     * @param width the new viewport width
     * @param height the new viewport height
     */
    public void reshape(int width, int height) {}
    
    /**
     * Called when the display mode or the display device associated with this
     * Canvas3D has changed, or when the library is to shut down. Applications
     * that wish to perform operations in one of these cases, may override this
     * method.<p>
     * NOTE: Applications should not call this method directly. It is called by
     * the Xenon3D rendering system.
     */
    public void dispose() {}

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Package Private Methods ">

    /**
     * Utility method to track when this Canvas3D is added to a container.
     * @param c the container to which to add the canvas
     * @throws IllegalStateException if the Canvas3D is already attached to a Container
     */
    void addNotify(Container c) {
        if (canvas != null) throw new IllegalStateException(Xenon3D.ERR_CANVAS_ALREADY_ATTACHED);
        canvas = new GLCanvas(caps);
        if (view != null) canvas.addGLEventListener(view.getInternalListener());
        c.add(canvas);
    }

    /**
     * Utility method to track when this Canvas3D is removed from its container.
     * @param c the container from which to remove the canvas
     */
    void removeNotify(Container c) {
        if (canvas == null) return;
        if (view != null) canvas.removeGLEventListener(view.getInternalListener());
        c.remove(canvas);
        canvas = null;
    }

    // </editor-fold>

} // end class Canvas3D
